---
title: Customization showcase
description: Interesting/unconventional layouts or behavior
# hide_table_of_contents: true
---

import { BrowserWindow } from '@site/src/components/BrowserWindow';
import { DemoLink } from '@site/src/components/DemoLink';
import { QueryBuilderEmbed } from '@site/src/components/QueryBuilderEmbed';
import { SandpackRQB } from '@site/src/components/SandpackRQB';
import TabItem from '@theme/TabItem';
import Tabs from '@theme/Tabs';

export const fields = [
  { name: 'firstName', label: 'First name' },
  { name: 'lastName', label: 'Last name' },
];

These examples showcase React Query Builder's extensive customization capabilities. Submit a pull request to share your interesting or unusual implementations!

### Justified layout

These CSS rules push "clone", "lock", or "remove" buttons to the right edge, creating a justified appearance. <DemoLink option="justifiedLayout" text="The demo has an option to enable this technique" />.

<details>
  <summary>CSS</summary>

{/* prettier-ignore */}
```css
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-cloneGroup,
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-lock,
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-remove,
.queryBuilder .rule-operators     + button.rule-cloneRule,
.queryBuilder .rule-operators     + button.rule-lock,
.queryBuilder .rule-operators     + button.rule-remove,
.queryBuilder .rule-value         + button.rule-cloneRule,
.queryBuilder .rule-value         + button.rule-lock,
.queryBuilder .rule-value         + button.rule-remove {
  margin-left: auto !important;
}
```

</details>

<BrowserWindow>
  <div className="justifiedLayout">
    <QueryBuilderEmbed
      fields={fields}
      showLockButtons
      showCloneButtons
      defaultQuery={{
        combinator: 'and',
        rules: [
          { field: 'firstName', operator: 'beginsWith', value: 'Stev' },
          {
            combinator: 'or',
            rules: [
              { field: 'lastName', operator: '=', value: 'Vai' },
              { field: 'lastName', operator: '=', value: 'Vaughan' },
              { field: 'lastName', operator: '=', value: 'Martin' },
            ],
          },
          { field: 'firstName', operator: 'null', value: null },
        ],
      }}
    />
  </div>
</BrowserWindow>

### Inline combinator selectors

Positions combinator selectors to the right of their preceding rules or groups.

:::note

These examples use [independent combinators](../components/querybuilder#independent-combinators), but the same styles work with [`showCombinatorsBetweenRules`](../components/querybuilder#showcombinatorsbetweenrules).

:::

<details>
  <summary>CSS</summary>

```css
.ruleGroup-body {
  /* Override the default flex layout */
  display: grid !important;
  /* Allow the left-hand column (the rule/subgroup) to expand as needed */
  /* Collapse the right-hand column (the combinator) to the width of the content */
  grid-template-columns: auto min-content;
  /* Keep the combinator aligned with the bottom of the rule/subgroup */
  align-items: end;
}
```

</details>

<BrowserWindow>
  <QueryBuilderEmbed
    fields={fields}
    controlClassnames={{ body: 'inline-indycomb' }}
    defaultQuery={{
      rules: [
        { field: 'firstName', operator: 'beginsWith', value: 'Stev' },
        'and',
        {
          rules: [
            { field: 'lastName', operator: '=', value: 'Vai' },
            'or',
            { field: 'lastName', operator: '=', value: 'Vaughan' },
            'or',
            { field: 'lastName', operator: '=', value: 'Martin' },
          ],
        },
        'or',
        { field: 'firstName', operator: 'null', value: null },
      ],
    }}
  />
</BrowserWindow>

Alternatively, position combinators to the left of their following rules or groups:

<details>
  <summary>CSS</summary>

```css
.ruleGroup-body {
  /* Override the default flex layout */
  display: grid !important;
  /* Allow the right-hand column (the rule/subgroup) to expand as needed */
  /* Collapse the left-hand column (the combinator) to the width of the content */
  grid-template-columns: min-content auto;
  /* Keep the combinator aligned with the top of the rule/subgroup */
  align-items: start;
}

/* Indent the first rule/subgroup since it has no preceding combinator */
.ruleGroup-body > .rule:first-child:not(:only-child),
.ruleGroup-body > .ruleGroup:first-child:not(:only-child) {
  grid-column-start: 2;
}
```

</details>

<BrowserWindow>
  <QueryBuilderEmbed
    fields={fields}
    controlClassnames={{ body: 'inline-indycomb-left' }}
    defaultQuery={{
      rules: [
        { field: 'firstName', operator: 'beginsWith', value: 'Stev' },
        'and',
        {
          rules: [
            { field: 'lastName', operator: '=', value: 'Vai' },
            'or',
            { field: 'lastName', operator: '=', value: 'Vaughan' },
            'or',
            { field: 'lastName', operator: '=', value: 'Martin' },
          ],
        },
        'or',
        { field: 'firstName', operator: 'null', value: null },
      ],
    }}
  />
</BrowserWindow>

## Disjunctive normal form

This example implements [disjunctive normal form (DNF)](https://en.wikipedia.org/wiki/Disjunctive_normal_form) by restricting root groups to "or", subgroups to "and", and limiting nesting to one level. Additional customizations include:

**CSS customizations:**

- Swap header and body order within groups
- Hide top-level "add rule" button (prevents rules in root group)
- Hide subgroup "add group" buttons (prevents deep nesting)
- Display subgroups horizontally
- Stack rule elements vertically (accommodates horizontal layout)
- Hide "remove group" buttons (would appear oddly positioned)

**Component/prop customizations:**

- Display combinators as static text (DNF is always "OR of ANDs")
- Remove groups when their last rule is deleted
- Auto-add default rules to new groups (enables immediate removal)

:::tip

This example demonstrates techniques detailed in the [arbitrary updates guide](./arbitrary-updates) and [hooks documentation](../utils/hooks#usequerybuilderquery).

:::

> _You may want to hide the left-hand sidebar (click `<<` at the bottom) to have a wider view of this example._

<SandpackRQB rqbVersion={7} options={{ editorHeight: 480 }}>

```css
.queryBuilder .ruleGroup .ruleGroup-header {
  /* Move group header below body. */
  /* You could also accomplish the same effect with */
  /* a custom RuleGroup or Rule component without */
  /* rearranging the header and body. Also note that */
  /* this method can cause problems with drag-and-drop. */
  order: 2;
}
.queryBuilder > .ruleGroup > .ruleGroup-header > .ruleGroup-remove {
  /* Hide "remove group" button */
  display: none;
}
.queryBuilder > .ruleGroup {
  flex-direction: row;
}
.queryBuilder > .ruleGroup > .ruleGroup-header {
  flex-shrink: 1 !important;
}
.queryBuilder > .ruleGroup > .ruleGroup-header > .ruleGroup-addRule {
  /* Hide "add rule" button from root group */
  display: none;
}
.queryBuilder .betweenRules {
  font-family: sans-serif;
}
.queryBuilder > .ruleGroup > .ruleGroup-body {
  width: 100%;
  flex-direction: row;
  align-items: stretch;
}
.queryBuilder > .ruleGroup > .ruleGroup-body > .ruleGroup {
  /* Extra spacing above the "add rule" buttons */
  gap: 1rem;
}
.queryBuilder > .ruleGroup > .ruleGroup-body .ruleGroup-addGroup {
  /* Hide "add group" button from "and" groups */
  display: none;
}
.queryBuilder > .ruleGroup > .ruleGroup-body > .betweenRules {
  /* Fix height of inline combinators */
  height: fit-content;
  /* Center vertically */
  margin-top: auto;
  margin-bottom: auto;
}
.queryBuilder > .ruleGroup > .ruleGroup-body > .ruleGroup {
  /* Span the full width of the "or" group body */
  width: 100%;
}
.queryBuilder > .ruleGroup > .ruleGroup-body > .ruleGroup .betweenRules {
  /* A little space between each combinator and the rule above it */
  margin-top: 1rem;
}
.queryBuilder > .ruleGroup .rule {
  /* Stack field/operator/value vertically */
  flex-direction: column;
  /* Left-align */
  align-items: flex-start;
}
.queryBuilder > .ruleGroup .rule > .rule-remove {
  /* Move "remove rule" button to top of rule */
  order: -1;
  /* Right-align "remove rule" button */
  margin-left: auto;
}
.queryBuilder .rule > .rule-value {
  max-width: 8rem;
}
```

```tsx
import { useState } from 'react';
import type {
  Field,
  RuleGroupType,
  ActionProps,
  CombinatorSelectorProps,
} from 'react-querybuilder';
import {
  ActionElement,
  getOption,
  QueryBuilder,
  findPath,
  getParentPath,
  remove,
  useQueryBuilderQuery,
} from 'react-querybuilder';

const fields: Field[] = [
  { name: 'firstName', label: 'First Name' },
  { name: 'lastName', label: 'Last Name' },
];

const initialQuery: RuleGroupType = {
  combinator: 'or',
  rules: [
    {
      combinator: 'and',
      rules: [
        { field: 'firstName', operator: 'beginsWith', value: 'Stev' },
        { field: 'lastName', operator: 'in', value: 'Vai,Vaughan' },
      ],
    },
    {
      combinator: 'and',
      rules: [
        {
          field: 'firstName',
          operator: 'beginsWith',
          value: 'To remove group, remove last rule',
        },
      ],
    },
  ],
};

const RemoveRule = (props: ActionProps) => {
  const query = useQueryBuilderQuery();
  if (!query) return null;
  const parentPath = getParentPath(props.path);
  /** Count of rules in the same group */
  const siblingCount = (findPath(parentPath, query) as RuleGroupType).rules.length - 1;
  const onClick = (e: React.MouseEvent) => {
    if (siblingCount > 0) {
      // If sibling rules exist, remove the current rule
      // using the default handler
      props.handleOnClick(e, props.context);
    } else {
      // Otherwise, remove the entire group
      props.schema.dispatchQuery(remove(query, getParentPath(props.path)));
    }
  };
  // Render with default props except for our custom click handler
  if (siblingCount === 0 && query.rules.length === 1) {
    // Don't render the "remove rule" action if this is
    // the only rule in the only group
    return null;
    // (You could also go ahead and render the button but
    // not remove its group in the click handler)
  }
  return <ActionElement {...props} handleOnClick={onClick} />;
};

const CombinatorSelector = (props: CombinatorSelectorProps) => (
  // Render static value to prevent combinator updates
  <div className={props.className} title={props.title}>
    {getOption(props.options, props.value!)?.label}
  </div>
);

export default () => {
  const [query, setQuery] = useState(initialQuery);

  return (
    <QueryBuilder
      fields={fields}
      query={query}
      onQueryChange={setQuery}
      // Move combinator control from group
      // header to group body:
      showCombinatorsBetweenRules
      // Ensure new groups are not empty:
      addRuleToNewGroups
      controlElements={{
        // Use our custom components
        removeRuleAction: RemoveRule,
        combinatorSelector: CombinatorSelector,
      }}
      translations={{
        addGroup: { label: '+' },
        addRule: { label: '+' },
      }}
    />
  );
};
```

</SandpackRQB>
